package sufutian.com.englishvoicetrans;

import android.content.Context;
import android.os.AsyncTask;
import android.support.annotation.NonNull;
import android.util.Log;

import com.google.auth.Credentials;
import com.google.auth.oauth2.AccessToken;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.cloud.speech.v1.LongRunningRecognizeRequest;
import com.google.cloud.speech.v1.RecognitionAudio;
import com.google.cloud.speech.v1.RecognitionConfig;
import com.google.cloud.speech.v1.RecognizeRequest;
import com.google.cloud.speech.v1.RecognizeResponse;
import com.google.cloud.speech.v1.SpeechGrpc;
import com.google.cloud.speech.v1.SpeechRecognitionAlternative;
import com.google.cloud.speech.v1.SpeechRecognitionResult;
import com.google.longrunning.Operation;
import com.google.protobuf.Any;
import com.google.protobuf.ByteString;

import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

import io.grpc.CallOptions;
import io.grpc.Channel;
import io.grpc.ClientCall;
import io.grpc.ClientInterceptor;
import io.grpc.ClientInterceptors;
import io.grpc.ManagedChannel;
import io.grpc.Metadata;
import io.grpc.MethodDescriptor;
import io.grpc.Status;
import io.grpc.StatusException;
import io.grpc.internal.DnsNameResolverProvider;
import io.grpc.okhttp.OkHttpChannelProvider;
import io.grpc.stub.StreamObserver;


/**
 * Created by sufutian on 2017/6/21.
 */

public class EnglishVoiceClient2 {

    public String TAG = "EnglishVoiceClient2";

    /**
     * 认证网址
     */
    private final List<String> OAUTH2_SCOPES = Arrays.asList("https://www.googleapis.com/auth/cloud-platform");

    private String host = "speech.googleapis.com";

    private Integer port = 443;

    private Context context;

//    private ManagedChannel managedChannel;

    private SpeechGrpc.SpeechStub speechClient;

    private final ArrayList<EnglishTransListener> mEnglishTransListeners = new ArrayList<>();

    private volatile AccessTokenTask mAccessTokenTask;

    public interface EnglishTransListener {

        /**
         * Called when a new piece of text was recognized by the Speech API.
         *
         * @param text    The text.
         * @param isFinal {@code true} when the API finished processing audio.
         */
        void onSpeechRecognized(String text, boolean isFinal);
    }

    public void addListener(@NonNull EnglishTransListener englishTransListener) {
        mEnglishTransListeners.add(englishTransListener);
    }

    public void removeListener(@NonNull EnglishTransListener englishTransListener) {
        mEnglishTransListeners.remove(englishTransListener);
    }

    private final StreamObserver<RecognizeResponse> mFileResponseObserver = new StreamObserver<RecognizeResponse>() {
        @Override
        public void onNext(RecognizeResponse response) {
            String text = null;
            if (response.getResultsCount() > 0) {
                final SpeechRecognitionResult result = response.getResults(0);
                if (result.getAlternativesCount() > 0) {
                    final SpeechRecognitionAlternative alternative = result.getAlternatives(0);
                    text = alternative.getTranscript();

                }
            }
            if (text != null) {
                for (EnglishTransListener englishTransListener : mEnglishTransListeners) {
                    englishTransListener.onSpeechRecognized(text, true);
                }
            }
        }

        @Override
        public void onError(Throwable t) {
            Log.e(TAG, "Error calling the API.", t);
        }

        @Override
        public void onCompleted() {
            Log.i(TAG, "API completed.");
        }

    };


    public EnglishVoiceClient2(Context context) {
        this.context = context;
        try {
            prepare(context.getResources().openRawResource(R.raw.credential));
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    private void prepare(InputStream authorizationFile) throws IOException {
        if (mAccessTokenTask != null) {
            return;
        }
        mAccessTokenTask = new AccessTokenTask(authorizationFile);
        mAccessTokenTask.execute();
    }


    private class AccessTokenTask extends AsyncTask<Void, Void, AccessToken> {

        private InputStream authorizationFile;

        public AccessTokenTask(InputStream authorizationFile) {
            this.authorizationFile = authorizationFile;
        }

        @Override
        protected AccessToken doInBackground(Void... voids) {
            try {

                final GoogleCredentials credentials = GoogleCredentials.fromStream(authorizationFile)
                        .createScoped(OAUTH2_SCOPES);
                final AccessToken token = credentials.refreshAccessToken();

                return token;
            } catch (IOException e) {
                Log.e(TAG, "Failed to obtain access token.", e);
            }
            return null;
        }

        @Override
        protected void onPostExecute(AccessToken accessToken) {
            mAccessTokenTask = null;
            final ManagedChannel channel = new OkHttpChannelProvider()
                    .builderForAddress(host, port)
                    .nameResolverFactory(new DnsNameResolverProvider())
                    .intercept(new GoogleCredentialsInterceptor(new GoogleCredentials(accessToken)
                            .createScoped(OAUTH2_SCOPES)))
                    .build();

            if (channel != null) {
                speechClient = SpeechGrpc.newStub(channel);
            }
        }
    }


    private class GoogleCredentialsInterceptor implements ClientInterceptor {

        private final Credentials mCredentials;

        private Metadata mCached;

        private Map<String, List<String>> mLastMetadata;

        GoogleCredentialsInterceptor(Credentials credentials) {
            mCredentials = credentials;
        }

        @Override
        public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
                final MethodDescriptor<ReqT, RespT> method, CallOptions callOptions,
                final Channel next) {
            return new ClientInterceptors.CheckedForwardingClientCall<ReqT, RespT>(
                    next.newCall(method, callOptions)) {
                @Override
                protected void checkedStart(Listener<RespT> responseListener, Metadata headers)
                        throws StatusException {
                    Metadata cachedSaved;
                    URI uri = serviceUri(next, method);
                    synchronized (this) {
                        Map<String, List<String>> latestMetadata = getRequestMetadata(uri);
                        if (mLastMetadata == null || mLastMetadata != latestMetadata) {
                            mLastMetadata = latestMetadata;
                            mCached = toHeaders(mLastMetadata);
                        }
                        cachedSaved = mCached;
                    }
                    headers.merge(cachedSaved);
                    delegate().start(responseListener, headers);
                }
            };
        }


        /**
         * Generate a JWT-specific service URI. The URI is simply an identifier with enough
         * information for a service to know that the JWT was intended for it. The URI will
         * commonly be verified with a simple string equality check.
         */
        private URI serviceUri(Channel channel, MethodDescriptor<?, ?> method)
                throws StatusException {
            String authority = channel.authority();
            if (authority == null) {
                throw Status.UNAUTHENTICATED
                        .withDescription("Channel has no authority")
                        .asException();
            }
            // Always use HTTPS, by definition.
            final String scheme = "https";
            final int defaultPort = 443;
            String path = "/" + MethodDescriptor.extractFullServiceName(method.getFullMethodName());
            URI uri;
            try {
                uri = new URI(scheme, authority, path, null, null);
            } catch (URISyntaxException e) {
                throw Status.UNAUTHENTICATED
                        .withDescription("Unable to construct service URI for auth")
                        .withCause(e).asException();
            }
            // The default port must not be present. Alternative ports should be present.
            if (uri.getPort() == defaultPort) {
                uri = removePort(uri);
            }
            return uri;
        }

        private Map<String, List<String>> getRequestMetadata(URI uri) throws StatusException {
            try {
                return mCredentials.getRequestMetadata(uri);
            } catch (IOException e) {
                throw Status.UNAUTHENTICATED.withCause(e).asException();
            }
        }

        private URI removePort(URI uri) throws StatusException {
            try {
                return new URI(uri.getScheme(), uri.getUserInfo(), uri.getHost(), -1 /* port */,
                        uri.getPath(), uri.getQuery(), uri.getFragment());
            } catch (URISyntaxException e) {
                throw Status.UNAUTHENTICATED
                        .withDescription("Unable to construct service URI after removing port")
                        .withCause(e).asException();
            }
        }

        private Metadata toHeaders(Map<String, List<String>> metadata) {
            Metadata headers = new Metadata();
            if (metadata != null) {
                for (String key : metadata.keySet()) {
                    Metadata.Key<String> headerKey = Metadata.Key.of(
                            key, Metadata.ASCII_STRING_MARSHALLER);
                    for (String value : metadata.get(key)) {
                        headers.put(headerKey, value);
                    }
                }
            }
            return headers;
        }
    }


    /**
     * Recognize all data from the specified {@link InputStream}.
     *
     * @param stream The audio data.
     */
    public void recognizeInputStream(final InputStream stream, final boolean longUPload) {
        if (speechClient == null) {
            return;
        }

        new Thread() {
            @Override
            public void run() {

                if (longUPload) {
                    try {
                        speechClient.longRunningRecognize(LongRunningRecognizeRequest.newBuilder()
                                        .setConfig(RecognitionConfig.newBuilder()
                                                .setEncoding(RecognitionConfig.AudioEncoding.LINEAR16)
                                                .setLanguageCode("en-US")
                                                .setSampleRateHertz(44100)
                                                .build())
                                        .setAudio(RecognitionAudio.newBuilder()
//                                                .setContent(ByteString.readFrom(stream))
                                                .setUri("gs://gcs-test-data/vr.flac")
                                                .build())
                                        .build()
                                , new StreamObserver<Operation>() {
                                    @Override
                                    public void onNext(Operation operation) {
                                        Any response = operation.getResponse();
                                    }

                                    @Override
                                    public void onError(Throwable t) {
                                        Log.e(TAG, "Error calling the API.", t);
                                    }

                                    @Override
                                    public void onCompleted() {
                                        Log.i(TAG, "API completed.");
                                    }
                                });
//                        stream.close();
                    } catch (Exception e) {
                        e.printStackTrace();
                        Log.e(TAG, "Error loading the input", e);
                    }
                } else {
                    try {
                        speechClient.recognize(
                                RecognizeRequest.newBuilder()
                                        .setConfig(RecognitionConfig.newBuilder()
                                                .setEncoding(RecognitionConfig.AudioEncoding.LINEAR16)
                                                .setLanguageCode("en-US")
                                                .setSampleRateHertz(16000)
                                                .build())
                                        .setAudio(RecognitionAudio.newBuilder()
                                                .setContent(ByteString.readFrom(stream))
                                                .build())
                                        .build(),
                                new StreamObserver<RecognizeResponse>() {
                                    @Override
                                    public void onNext(RecognizeResponse response) {
                                        String text = null;
                                        if (response.getResultsCount() > 0) {
                                            final SpeechRecognitionResult result = response.getResults(0);
                                            if (result.getAlternativesCount() > 0) {
                                                final SpeechRecognitionAlternative alternative = result.getAlternatives(0);
                                                text = alternative.getTranscript();

                                            }
                                        }
                                        if (text != null) {
                                            for (EnglishTransListener englishTransListener : mEnglishTransListeners) {
                                                englishTransListener.onSpeechRecognized(text, true);
                                            }
                                        }
                                    }

                                    @Override
                                    public void onError(Throwable t) {
                                        Log.e(TAG, "Error calling the API.", t);
                                    }

                                    @Override
                                    public void onCompleted() {
                                        Log.i(TAG, "API completed.");
                                    }
                                });
//                        stream.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                        Log.e(TAG, "Error loading the input", e);
                    }
                }

            }
        }.start();

    }
}
